package bigbrother.core;

import bigbrother.IrcBotContext;
import bigbrother.IrcBotContextImpl;
import bigbrother.modules.Module;
import bigbrother.modules.help.HelpModule;
import org.jibble.pircbot.DccChat;
import org.jibble.pircbot.DccFileTransfer;
import org.jibble.pircbot.PircBot;
import org.jibble.pircbot.User;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.logging.Logger;

/**
 * Core of an irc bot, delegates commands to different modules.
 *
 * @author Hans Haggstrom
 */
// TODO: Split into generic IRC utility base class and IRC bot specific subclass that implements the command handling.  
@SuppressWarnings( { "TypeMayBeWeakened" } )
public class IrcBotCore
        extends PircBot
{

    //======================================================================
    // Private Fields

    private final Properties myProperties;
    private final Map<String, Module> myModules = new HashMap<String, Module>();
    private final String myVersion;
    private final String myModel;

    private List<Module> myActiveModules = new ArrayList<Module>();
    private List<String> myCommandPrefixes = new ArrayList<String>();

    private final IrcBotContext myIrcBotContext;

    //======================================================================
    // Private Constants

    private static final String DEFAULT_SERVER = "localhost";
    private static final String DEFAULT_CHANNEL = "#test";

    private static final Logger LOGGER = Logger.getLogger( IrcBotCore.class.getName() );

    //======================================================================
    // Public Methods

    //----------------------------------------------------------------------
    // Constructors

    /**
     * Creates an {@link IrcBotCore}.
     *
     * @param ircBotName         the name of this type of IRC bot (used as default for the nick name too, although the
     *                           nick name can be specified in the config file also).
     * @param version            the version of this IRC bot.
     * @param configFileLocation the location of the config file to use.  The config file is both used to read settings
     *                           from, as well as to store various module specific settings that are edited by users
     *                           (e.g. if RSS feeds are added, etc).
     */
    public IrcBotCore( final String ircBotName, final String version, final String configFileLocation )
    {
        // TODO: Get parameter checker utility, and make sure these are correct.
        myVersion = version;
        myModel = ircBotName;

        LOGGER.info( "Loading configuration from '" + configFileLocation + "'." );
        myProperties = loadProperties( configFileLocation );

        setLogin( getBotModel() );
        setNickName( myProperties.getProperty( "nick", myModel ) );

        final File logFileStorageDirectory = new File( myProperties.getProperty( "logger.logDirectory", "logs" ) );
        myIrcBotContext = new IrcBotContextImpl( this, logFileStorageDirectory );
    }

    //----------------------------------------------------------------------
    // Other Public Methods

    /**
     * @return the model of this bot, as well as its version number.
     */
    public final synchronized String getBotModelAndVersion()
    {
        return getBotModel() + " version " + getBotVersion();
    }


    /**
     * @return the model name of this bot.
     */
    public final String getBotModel()
    {
        return myModel;
    }


    /**
     * @return the version number of this bot, in major.minor.mini version numbering scheme.  May have postponed release
     *         candidate, Draft, and other tags.
     */
    public final String getBotVersion()
    {
        return myVersion;
    }


    /**
     * @return the modules that are currently working and active.
     */
    public final synchronized List<Module> getActiveModules()
    {
        return new ArrayList<Module>( myActiveModules );
    }


    /**
     * Adds the specified Module.
     *
     * @param addedModule should not be null or already added.
     */
    public final void addIrcModule( Module addedModule )
    {
        if ( addedModule == null )
        {
            throw new IllegalArgumentException( "addedModule should not be null" );
        }
        if ( myModules.containsKey( addedModule.getIdentifier() ) )
        {
            throw new IllegalArgumentException( "thee is already a module with the identifier '" + addedModule.getIdentifier() + "' " );
        }

        myModules.put( addedModule.getIdentifier(), addedModule );
    }


    /**
     * Removes the specified Module.
     *
     * @param removedModule should not be null, and should be present.
     */
    public final void removeIrcModule( Module removedModule )
    {
        if ( myActiveModules.contains( removedModule ) )
        {
            stopModule( removedModule.getIdentifier() );
        }

        myModules.remove( removedModule.getIdentifier() );
    }


    /**
     * Start the bot and join the server.
     */
    public final void startIrcBot()
    {
        final String server = myProperties.getProperty( "server", DEFAULT_SERVER );
        final String channel = myProperties.getProperty( "channel", DEFAULT_CHANNEL );

        try
        {
            LOGGER.info( "Connecting to '" + server + "'." );
            connect( server );
        }
        catch ( Exception e )
        {
            LOGGER.severe( "Could not connect to '" + server + "' : " + e.getMessage() );
        }

        LOGGER.info( "Joining channel '" + channel + "'." );
        joinChannel( channel );

        // Start modules
        for ( String module : myModules.keySet() )
        {
            startModule( module );
        }
    }


    /**
     * @return the first registered command prefix.  By default, '!'.
     */
    public final String getDefaultCommandPrefix()
    {
        return myCommandPrefixes.get( 0 );
    }

    //======================================================================
    // Protected Methods

    @Override
    protected void onConnect()
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onConnect();
        }
    }


    @Override
    protected void onDisconnect()
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onDisconnect();
        }
    }


    @Override
    protected void onServerResponse( final int code, final String response )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onServerResponse( code, response );
        }
    }


    @Override
    protected void onUserList( final String channel, final User[] users )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onUserList( channel, users );
        }
    }


    @Override
    protected void onMessage( final String channel,
                              final String sender,
                              final String login,
                              final String hostname,
                              final String message )
    {
        // TODO: We need to wrap all calls of modules in some try catches to catch errors.  So have one class that converts from separate functions into message objects, and allow modules to register message handlers for different types of messages?
        try
        {
            checkForCommand( channel, sender, login, hostname, message );

            for ( Module activeModule : myActiveModules )
            {
                activeModule.onMessage( channel, sender, login, hostname, message );
            }
        }
        catch ( Exception e )
        {
            // DEBUG
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }


    @Override
    protected void onPrivateMessage( final String sender,
                                     final String login,
                                     final String hostname,
                                     final String message )
    {
        try
        {
            checkForCommand( null, sender, login, hostname, message );

            for ( Module activeModule : myActiveModules )
            {
                activeModule.onPrivateMessage( sender, login, hostname, message );
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }


    @Override
    protected void onAction( final String sender,
                             final String login,
                             final String hostname,
                             final String target,
                             final String action )
    {
        try
        {
            for ( Module activeModule : myActiveModules )
            {
                activeModule.onAction( sender, login, hostname, target, action );
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }


    @Override
    protected void onNotice( final String sourceNick,
                             final String sourceLogin,
                             final String sourceHostname,
                             final String target,
                             final String notice )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onNotice( sourceNick,
                                   sourceLogin,
                                   sourceHostname,
                                   target,
                                   notice );
        }
    }


    @Override
    protected void onJoin( final String channel, final String sender, final String login, final String hostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onJoin( channel, sender, login, hostname );
        }
    }


    @Override
    protected void onPart( final String channel, final String sender, final String login, final String hostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onPart( channel, sender, login, hostname );
        }
    }


    @Override
    protected void onNickChange( final String oldNick, final String login, final String hostname, final String newNick )
    {
        // In case it was the bot that changed nicks..
        updateCommandPrefixes( getName() );

        for ( Module activeModule : myActiveModules )
        {
            activeModule.onNickChange( oldNick, login, hostname, newNick );
        }
    }


    @Override
    protected void onKick( final String channel,
                           final String kickerNick,
                           final String kickerLogin,
                           final String kickerHostname,
                           final String recipientNick,
                           final String reason )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onKick( channel,
                                 kickerNick,
                                 kickerLogin,
                                 kickerHostname,
                                 recipientNick,
                                 reason );
        }
    }


    @Override
    protected void onQuit( final String sourceNick,
                           final String sourceLogin,
                           final String sourceHostname,
                           final String reason )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onQuit( sourceNick,
                                 sourceLogin,
                                 sourceHostname,
                                 reason );
        }
    }


    @Override
    protected void onTopic( final String channel,
                            final String topic,
                            final String setBy,
                            final long date,
                            final boolean changed )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onTopic( channel, topic, setBy, date, changed );
        }
    }


    @Override
    protected void onChannelInfo( final String channel, final int userCount, final String topic )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onChannelInfo( channel, userCount, topic );
        }
    }


    @Override
    protected void onMode( final String channel,
                           final String sourceNick,
                           final String sourceLogin,
                           final String sourceHostname,
                           final String mode )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onMode( channel,
                                 sourceNick,
                                 sourceLogin,
                                 sourceHostname,
                                 mode );
        }
    }


    @Override
    protected void onUserMode( final String targetNick,
                               final String sourceNick,
                               final String sourceLogin,
                               final String sourceHostname,
                               final String mode )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onUserMode( targetNick,
                                     sourceNick,
                                     sourceLogin,
                                     sourceHostname,
                                     mode );
        }
    }


    @Override
    protected void onOp( final String channel,
                         final String sourceNick,
                         final String sourceLogin,
                         final String sourceHostname,
                         final String recipient )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onOp( channel,
                               sourceNick,
                               sourceLogin,
                               sourceHostname,
                               recipient );
        }
    }


    @Override
    protected void onDeop( final String channel,
                           final String sourceNick,
                           final String sourceLogin,
                           final String sourceHostname,
                           final String recipient )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onDeop( channel,
                                 sourceNick,
                                 sourceLogin,
                                 sourceHostname,
                                 recipient );
        }
    }


    @Override
    protected void onVoice( final String channel,
                            final String sourceNick,
                            final String sourceLogin,
                            final String sourceHostname,
                            final String recipient )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onVoice( channel,
                                  sourceNick,
                                  sourceLogin,
                                  sourceHostname,
                                  recipient );
        }
    }


    @Override
    protected void onDeVoice( final String channel,
                              final String sourceNick,
                              final String sourceLogin,
                              final String sourceHostname,
                              final String recipient )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onDeVoice( channel,
                                    sourceNick,
                                    sourceLogin,
                                    sourceHostname,
                                    recipient );
        }
    }


    @Override
    protected void onSetChannelKey( final String channel,
                                    final String sourceNick,
                                    final String sourceLogin,
                                    final String sourceHostname,
                                    final String key )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetChannelKey( channel,
                                          sourceNick,
                                          sourceLogin,
                                          sourceHostname,
                                          key );
        }
    }


    @Override
    protected void onRemoveChannelKey( final String channel,
                                       final String sourceNick,
                                       final String sourceLogin,
                                       final String sourceHostname,
                                       final String key )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveChannelKey( channel,
                                             sourceNick,
                                             sourceLogin,
                                             sourceHostname,
                                             key );
        }
    }


    @Override
    protected void onSetChannelLimit( final String channel,
                                      final String sourceNick,
                                      final String sourceLogin,
                                      final String sourceHostname,
                                      final int limit )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetChannelLimit( channel,
                                            sourceNick,
                                            sourceLogin,
                                            sourceHostname,
                                            limit );
        }
    }


    @Override
    protected void onRemoveChannelLimit( final String channel,
                                         final String sourceNick,
                                         final String sourceLogin,
                                         final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveChannelLimit( channel,
                                               sourceNick,
                                               sourceLogin,
                                               sourceHostname );
        }
    }


    @Override
    protected void onSetChannelBan( final String channel,
                                    final String sourceNick,
                                    final String sourceLogin,
                                    final String sourceHostname,
                                    final String hostmask )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetChannelBan( channel,
                                          sourceNick,
                                          sourceLogin,
                                          sourceHostname,
                                          hostmask );
        }
    }


    @Override
    protected void onRemoveChannelBan( final String channel,
                                       final String sourceNick,
                                       final String sourceLogin,
                                       final String sourceHostname,
                                       final String hostmask )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveChannelBan( channel,
                                             sourceNick,
                                             sourceLogin,
                                             sourceHostname,
                                             hostmask );
        }
    }


    @Override
    protected void onSetTopicProtection( final String channel,
                                         final String sourceNick,
                                         final String sourceLogin,
                                         final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetTopicProtection( channel,
                                               sourceNick,
                                               sourceLogin,
                                               sourceHostname );
        }
    }


    @Override
    protected void onRemoveTopicProtection( final String channel,
                                            final String sourceNick,
                                            final String sourceLogin,
                                            final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveTopicProtection( channel,
                                                  sourceNick,
                                                  sourceLogin,
                                                  sourceHostname );
        }
    }


    @Override
    protected void onSetNoExternalMessages( final String channel,
                                            final String sourceNick,
                                            final String sourceLogin,
                                            final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetNoExternalMessages( channel,
                                                  sourceNick,
                                                  sourceLogin,
                                                  sourceHostname );
        }
    }


    @Override
    protected void onRemoveNoExternalMessages( final String channel,
                                               final String sourceNick,
                                               final String sourceLogin,
                                               final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveNoExternalMessages( channel,
                                                     sourceNick,
                                                     sourceLogin,
                                                     sourceHostname );
        }
    }


    @Override
    protected void onSetInviteOnly( final String channel,
                                    final String sourceNick,
                                    final String sourceLogin,
                                    final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetInviteOnly( channel,
                                          sourceNick,
                                          sourceLogin,
                                          sourceHostname );
        }
    }


    @Override
    protected void onRemoveInviteOnly( final String channel,
                                       final String sourceNick,
                                       final String sourceLogin,
                                       final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveInviteOnly( channel,
                                             sourceNick,
                                             sourceLogin,
                                             sourceHostname );
        }
    }


    @Override
    protected void onSetModerated( final String channel,
                                   final String sourceNick,
                                   final String sourceLogin,
                                   final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetModerated( channel,
                                         sourceNick,
                                         sourceLogin,
                                         sourceHostname );
        }
    }


    @Override
    protected void onRemoveModerated( final String channel,
                                      final String sourceNick,
                                      final String sourceLogin,
                                      final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveModerated( channel,
                                            sourceNick,
                                            sourceLogin,
                                            sourceHostname );
        }
    }


    @Override
    protected void onSetPrivate( final String channel,
                                 final String sourceNick,
                                 final String sourceLogin,
                                 final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetPrivate( channel,
                                       sourceNick,
                                       sourceLogin,
                                       sourceHostname );
        }
    }


    @Override
    protected void onRemovePrivate( final String channel,
                                    final String sourceNick,
                                    final String sourceLogin,
                                    final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemovePrivate( channel,
                                          sourceNick,
                                          sourceLogin,
                                          sourceHostname );
        }
    }


    @Override
    protected void onSetSecret( final String channel,
                                final String sourceNick,
                                final String sourceLogin,
                                final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onSetSecret( channel,
                                      sourceNick,
                                      sourceLogin,
                                      sourceHostname );
        }
    }


    @Override
    protected void onRemoveSecret( final String channel,
                                   final String sourceNick,
                                   final String sourceLogin,
                                   final String sourceHostname )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onRemoveSecret( channel,
                                         sourceNick,
                                         sourceLogin,
                                         sourceHostname );
        }
    }


    @Override
    protected void onInvite( final String targetNick,
                             final String sourceNick,
                             final String sourceLogin,
                             final String sourceHostname,
                             final String channel )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onInvite( targetNick,
                                   sourceNick,
                                   sourceLogin,
                                   sourceHostname,
                                   channel );
        }
    }


    @Override
    protected void onIncomingFileTransfer( final DccFileTransfer transfer )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onIncomingFileTransfer( transfer );
        }
    }


    @Override
    protected void onFileTransferFinished( final DccFileTransfer transfer, final Exception e )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onFileTransferFinished( transfer, e );
        }
    }


    @Override
    protected void onIncomingChatRequest( final DccChat chat )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onIncomingChatRequest( chat );
        }
    }


    @Override
    protected void onUnknown( final String line )
    {
        for ( Module activeModule : myActiveModules )
        {
            activeModule.onUnknown( line );
        }
    }

    //======================================================================
    // Private Methods

    /**
     * Start the module with the specified name.
     */
    private void startModule( String identifier )
    {
        if ( !myModules.containsKey( identifier ) )
        {
            throw new IllegalArgumentException( "there is no module with the identifier '" + identifier + "' " );
        }

        final Module module = myModules.get( identifier );

        if ( !myActiveModules.contains( module ) )
        {
            myActiveModules.add( module );

            LOGGER.info( "Starting Module '" + identifier + "'." );
            module.start( myIrcBotContext, myProperties );
        }
    }


    /**
     * Stops the module with the specified name.
     */
    private void stopModule( String identifier )
    {
        if ( !myModules.containsKey( identifier ) )
        {
            throw new IllegalArgumentException( "there is no module with the identifier '" + identifier + "' " );
        }

        final Module module = myModules.get( identifier );

        if ( myActiveModules.contains( module ) )
        {
            myActiveModules.remove( module );

            LOGGER.info( "Stopping Module '" + identifier + "'." );
            module.stop();
        }
    }


    private void setNickName( final String nick )
    {
        LOGGER.info( "Setting nickname to '" + nick + "'." );
        setName( nick );
        // TODO: If connected to a server, changeNick should be used instead?
        updateCommandPrefixes( nick );
    }


    private void checkForCommand( final String channel,
                                  final String sender,
                                  final String login,
                                  final String hostname,
                                  final String message )
    {
        for ( String commandPrefix : myCommandPrefixes )
        {
            if ( startsWithIgnoreCase( message, commandPrefix ) )
            {
                final String commandAfterCommandPrefix = message.substring( commandPrefix.length() ).trim();
                final String[] commandAndParameters = commandAfterCommandPrefix.split( " ", 2 );
                if ( commandAndParameters.length >= 1 )
                {
                    final String[] moduleAndCommand = commandAndParameters[ 0 ].split( "\\.", 2 );

                    // Get module name
                    final String moduleName = moduleAndCommand[ 0 ].trim();

                    // Get command name
                    String commandName = "";
                    if ( moduleAndCommand.length >= 2 )
                    {
                        commandName = moduleAndCommand[ 1 ];
                    }

                    String parameters = null;
                    if ( commandAndParameters.length >= 2 )
                    {
                        parameters = commandAndParameters[ 1 ];
                    }

                    // Get the module with the specified name
                    final Module module = myModules.get( moduleName );
                    if ( myActiveModules.contains( module ) )
                    {
                        module.handleCommand( channel, sender, login, hostname, commandName, parameters );
                    }
                    else
                    {
                        // If unknown command, send help message
                        sendMessage( channel,
                                     "This is " + getBotModelAndVersion() + ".  " +
                                     "Use " + getDefaultCommandPrefix() + HelpModule.HELP_MODULE_NAME + " for help." );
                    }
                }

                return;
            }
        }
    }


    /**
     * Sets up the prefixes that indicates that the rest of the line is a command for this bot, if they occur at the
     * start of a chat message.  There is be a fixed command prefix such as !, which can be configured in the properties
     * file.  In addition the current nick name of the bot can be used as a command prefix.
     *
     * @param botNickName the current nick name of the bot.  It can also be used as a prefix for irc bot commands.
     */
    private void updateCommandPrefixes( final String botNickName )
    {
        myCommandPrefixes.clear();
        myCommandPrefixes.add( myProperties.getProperty( "commandPrefix", "!" ) );
        myCommandPrefixes.add( botNickName + ":" );
        myCommandPrefixes.add( botNickName + "," );
        myCommandPrefixes.add( botNickName + " " );
    }


    /**
     * @return true if the text starts with the specified start string, ignoring upper or lower case differences.
     */
    // TODO: Move to some StringUtils class
    private static boolean startsWithIgnoreCase( final String text, final String start )
    {
        if ( text.length() >= start.length() )
        {
            final String s = text.substring( 0, start.length() );
            return start.equalsIgnoreCase( s );
        }
        else
        {
            return false;
        }
    }


    /**
     * @return the properties file, or an empty properties file if it didn't exist or could not be loaded.
     */
    // TODO: Move to some property utils class or similar?
    private Properties loadProperties( String propertyFileLocation )
    {
        final Properties properties = new Properties();
        FileInputStream stream = null;
        try
        {
            stream = new FileInputStream( propertyFileLocation );
            properties.load( stream );
            LOGGER.info( "Properties loaded." );
        }
        catch ( FileNotFoundException e )
        {
            LOGGER.severe( e.getMessage() );
        }
        catch ( IOException e )
        {
            LOGGER.severe( e.getMessage() );
        }
        finally
        {
            if ( stream != null )
            {
                try
                {
                    stream.close();
                }
                catch ( IOException e )
                {
                    LOGGER.warning( e.getMessage() );
                }
            }
        }
        return properties;
    }

}
